using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;

using BizElements.Core.ProviderComponents;

namespace BizElements.Core
{
    /// <summary>Strongly typed entity collection.</summary>
    /// <typeparam name="TEntity">Type of entities which are stored in the collection.</typeparam>
    /// <typeparam name="TTable">Table/view metadata and object factory for entities stored in the collection.</typeparam>
    /// <remarks>In .NET 2.0 or newer, instead of using generated collections, you may want to use this
    /// generic <b>EntityCollection&lt;TEntity, TTable&gt;</b> implementation. It makes it easier to define 
    /// new strong-typed collections which contain objects derived from generated entity classes.</remarks>
    [Serializable]
    public sealed class EntityCollection<TEntity, TTable> : IEntityCollection, IList<TEntity>, ICollection<TEntity>, IEnumerable<TEntity>
        where TEntity : class, IEntity, new()
        where TTable : IDbTable, new()
    {
        #region Private members.

        IList<TEntity> list;

        /// <summary>
        /// Alias of the table/view to which this entity belongs to.
        /// Required as distinct member when deserializing object to recreate member 'table'.
        /// </summary>
        private string tableAlias;

        /// <summary>
        /// Holds meta data for the table/view this entity belongs to.
        /// Not serialized because it is very big. This member should never be acessed directly.
        /// Use _Table property as it will recreate this member if it's null (eg. after deserialization).
        /// </summary>
        [NonSerialized]
        private TTable table;

        #endregion

        #region Constructors.

        /// <summary>
        /// Initializes a new instance of the EntityCollection class.
        /// </summary>
        /// <remarks>Creates a new EntityCollection for storing entities compatible with the specified table type.</remarks>
        public EntityCollection()
        {
            this.list = new List<TEntity>();
            this.table = new TTable();         
        }

        /// <summary>
        /// Initializes a new instance of the EntityCollection class.
        /// </summary>
        /// <param name="capacity">The number of elements that the new list can initially store.</param>
        /// <remarks>Creates a new EntityCollection for storing entities compatible with the specified table type.</remarks>
        public EntityCollection(int capacity)
        {
            this.list = new List<TEntity>(capacity);
            this.table = new TTable();
        }

        /// <summary>
        /// Initializes a new instance of the EntityCollection class.
        /// </summary>
        /// <param name="list">The list that is wrapped by the new collection.</param>
        public EntityCollection(IList<TEntity> list)
        {
            this.list = list;
            this.table = new TTable();
        }

        /// <summary>
        /// Initializes a new instance of the EntityCollection class.
        /// </summary>
        /// <param name="dbTable">Contains metadata about entities stored in the EntityCollection.</param>
        /// <remarks>Creates a new EntityCollection for storing entities compatible with the specified table.</remarks>
        public EntityCollection(TTable dbTable)
        {
            this.list = new List<TEntity>();
            this.table = dbTable;
            // Set info required by Activator.
            if (dbTable.Alias != dbTable.TableName)
                this.tableAlias = dbTable.Alias;
        }

        /// <summary>
        /// Initializes a new instance of the EntityCollection class.
        /// </summary>
        /// <param name="dbTable">Contains metadata about entities stored in the EntityCollection.</param>
        /// <param name="capacity">The number of elements that the new list can initially store.</param>
        /// <remarks>Creates a new EntityCollection for storing entities compatible with the specified table.</remarks>
        public EntityCollection(TTable dbTable, int capacity)
        {
            this.list = new List<TEntity>(capacity);
            this.table = dbTable;
            // Set info required by Activator.
            if (dbTable.Alias != dbTable.TableName)
                this.tableAlias = dbTable.Alias;
        }

        /// <summary>
        /// Initializes a new instance of the EntityCollection class.
        /// </summary>
        /// <param name="dbTable">Contains metadata about entities stored in the EntityCollection.</param>
        /// <param name="list">The list that is wrapped by the new collection.</param>
        /// <remarks>Creates a new EntityCollection for storing entities compatible with the specified table.</remarks>
        public EntityCollection(TTable dbTable, IList<TEntity> list)
        {
            this.list = list;
            this.table = dbTable;
            // Set info required by Activator.
            if (dbTable.Alias != dbTable.TableName)
                this.tableAlias = dbTable.Alias;
        }

        /// <summary>
        /// Initializes a new instance of the EntityCollection class.
        /// </summary>
        /// <param name="data">Contains entity data. Must be compatibile with entities stored in the collection.</param>
        /// <remarks>Creates a new EntityCollection for storing entities compatibile with the specified table type and 
        /// populates it with data from the given DataTable.</remarks>
        public EntityCollection(DataTable data)
        {
            this.list = new List<TEntity>(data.Rows.Count);
            EntityCollectionExtensions.FromDataTable<TEntity>(this, data, new TTable());
        }

        /// <summary>
        /// Initializes a new instance of the EntityCollection class.
        /// </summary>
        /// <param name="dbTable">Contains metadata about entities stored in the EntityCollection.</param>
        /// <param name="data">Contains entity data. Must be compatibile with entities stored in the collection.</param>
        public EntityCollection(TTable dbTable, DataTable data)
            : this(dbTable)
        {
            this.list = new List<TEntity>(data.Rows.Count);
            EntityCollectionExtensions.FromDataTable<TEntity>(this, data, dbTable);
        }

        #endregion

        #region Properties and indexers.

        /// <summary>
        /// Gets metadata for entities stored in the collection.
        /// </summary>
        public TTable Table
        {
            get
            {
                // Member 'table' may be null after deserialization. It has to be recreated.
                if (this.table == null)
                    this.table = DbTableFactory.CreateTable<TTable>(this.tableAlias);

                return this.table;
            }
        }

        /// <summary>Gets IDbTable object used by this EntityCollection.</summary>
        public IDbTable IDbTable
        {
            get { return this.Table; }
        }

        /// <summary>Gets or sets entity at the specified zero-based index.</summary>
        /// <param name="index">The zero-based index of the element to get or set.</param>
        IEntity IEntityCollection.this[int index]
        {
            get { return this[index]; }
            set { this[index] = (TEntity)value; }    
        }

        /// <summary>Gets the number of elements actually contained in the collection</summary>
        public int Count
        {
            get { return this.list.Count; }
        }

        /// <summary>Gets or sets entity at the specified zero-based index.</summary>
        /// <param name="index">The zero-based index of the element to get or set.</param>
        /// <value><b>null</b> is not allowed.</value>
        public TEntity this[int index]
        {
            get { return this.list[index]; }
            set
            {
                ValidateEntityNotNull(value);
                this.list[index] = value;
            }
        }

        #endregion

        #region ValidateEntityNotNull.

        private static void ValidateEntityNotNull(IEntity entity)
        {
            if (entity == null)
                throw new ArgumentNullException("entity", Messages.EntityCollection_NullsNotAllowed);
        }

        #endregion

        #region Collection<T> methods.

        /// <summary>Adds an entity to the end of the collection.</summary>
        /// <param name="item">The entity to be added to the end of the EntityCollection. <b>null</b> is not allowed.</param>
        public void Add(TEntity item)
        {
            ValidateEntityNotNull(item);
            this.list.Add(item);
        }

        /// <summary>Removes all elements from the collection.</summary>
        public void Clear()
        {
            this.list.Clear();
        }

        /// <summary>Determines whether an element is in the collection.</summary>
        /// <param name="item">The object to locate in the collection.</param>
        /// <returns><b>true</b> if item is found in the collection; otherwise, <b>false</b>.</returns>
        public bool Contains(TEntity item)
        {
            return this.list.Contains(item);
        }

        /// <summary>Copies the entire collection to a compatible one-dimensional Array, starting at the specified index of the target array.</summary>
        /// <param name="array">The one-dimensional Array that is the destination of the elements copied from collection. The Array must have zero-based indexing.</param>
        /// <param name="arrayIndex">The zero-based index in array at which copying begins.</param>
        public void CopyTo(TEntity[] array, int arrayIndex)
        {
            this.list.CopyTo(array, arrayIndex);
        }

        /// <summary>Returns an enumerator that iterates through the collection.</summary>
        /// <returns>An IEnumerator for the collection.</returns>
        public IEnumerator<TEntity> GetEnumerator()
        {
            return this.list.GetEnumerator();
        }

        /// <summary>Searches for the specified object and returns the zero-based index of the first occurrence within the entire collection.</summary>
        /// <param name="item">The object to locate in the collection.</param>
        /// <returns>The zero-based index of the first occurrence of item within the entire collection, if found; otherwise, -1.</returns>
        public int IndexOf(TEntity item)
        {
            return this.list.IndexOf(item);
        }

        /// <summary>Inserts an element into the collection at the specified index.</summary>
        /// <param name="index">The zero-based index at which item should be inserted.</param>
        /// <param name="item">The object to insert. <b>null</b> is not allowed.</param>
        public void Insert(int index, TEntity item)
        {
            ValidateEntityNotNull(item);
            this.list.Insert(index, item);
        }

        /// <summary>Removes the first occurrence of a specific object from the collection.</summary>
        /// <param name="item">The object to remove from the collection.</param>
        /// <returns>true if item is successfully removed; otherwise, false. This method also returns false if item was not found in the original collection.</returns>
        public bool Remove(TEntity item)
        {
            return this.list.Remove(item);
        }

        /// <summary>Removes the element at the specified index of the collection.</summary>
        /// <param name="index">The zero-based index of the element to remove.</param>
        public void RemoveAt(int index)
        {
            this.list.RemoveAt(index);
        }

        #endregion

        #region Explicit IList, ICollection, IEnumerable methods.

        /// <summary>Adds an item to the IList.</summary>
        /// <param name="value">The object to add to the IList.</param>
        /// <returns>The position into which the new element was inserted.</returns>
        int IList.Add(object value)
        {
            Add((TEntity)value);
            return this.list.Count - 1;
        }

        /// <summary>Determines whether an element is in the collection.</summary>
        /// <param name="value">The object to locate in the collection.</param>
        /// <returns><b>true</b> if item is found in the collection; otherwise, <b>false</b>.</returns>
        bool IList.Contains(object value)
        {
            return (this.list as IList).Contains(value);
        }

        /// <summary>Copies the entire collection to a compatible one-dimensional Array, starting at the specified index of the target array.</summary>
        /// <param name="array">The one-dimensional Array that is the destination of the elements copied from collection. The Array must have zero-based indexing.</param>
        /// <param name="index">The zero-based index in array at which copying begins.</param>
        public void CopyTo(Array array, int index)
        {
            (this.list as ICollection).CopyTo(array, index);
        }

        /// <summary>Returns an enumerator that iterates through the collection.</summary>
        /// <returns>An IEnumerator for the collection.</returns>
        IEnumerator IEnumerable.GetEnumerator()
        {            
            return this.list.GetEnumerator();
        }

        /// <summary>Searches for the specified object and returns the zero-based index of the first occurrence within the entire collection.</summary>
        /// <param name="item">The object to locate in the collection.</param>
        /// <returns>The zero-based index of the first occurrence of item within the entire collection, if found; otherwise, -1.</returns>
        int IList.IndexOf(object item)
        {
            return (this.list as IList).IndexOf(item);
        }

        /// <summary>Inserts an element into the collection at the specified index.</summary>
        /// <param name="index">The zero-based index at which item should be inserted.</param>
        /// <param name="value">The object to insert. <b>null</b> is not allowed.</param>
        void IList.Insert(int index, object value)
        {
            Insert(index, (TEntity)value);
        }

        /// <summary>Gets a value indicating whether the IList has a fixed size.</summary>
        /// <value>true if the IList has a fixed size; otherwise, false.</value>
        bool IList.IsFixedSize
        {
            get { return (this.list as IList).IsFixedSize; }
        }

        /// <summary>Gets a value indicating whether the ICollection is read-only.</summary>
        /// <value>true if the ICollection is read-only; otherwise, false.</value>
        bool ICollection<TEntity>.IsReadOnly
        {
            get { return this.list.IsReadOnly; }
        }

        /// <summary>Gets a value indicating whether the IList is read-only.</summary>
        /// <value>true if the ICollection is read-only; otherwise, false.</value>
        bool IList.IsReadOnly
        {
            get { return this.list.IsReadOnly; }
        }

        /// <summary>Gets a value indicating whether access to the ICollection is synchronized (thread safe).</summary>
        /// <value>true if access to the ICollection is synchronized (thread safe); otherwise, false.</value>
        bool ICollection.IsSynchronized
        {
            get { return (this.list as ICollection).IsSynchronized; }
        }

        /// <summary>Gets or sets the element at the specified index.</summary>
        /// <value>The element at the specified index. <b>null</b> is not allowed.</value>
        object IList.this[int index]
        {
            get { return this[index]; }
            set { this[index] = (TEntity)value; }
        }

        /// <summary>Removes the first occurrence of a specific object from the IList.</summary>
        /// <param name="value">The Object to remove from the IList.</param>
        void IList.Remove(object value)
        {
            (this.list as IList).Remove(value);
        }

        /// <summary>Gets an object that can be used to synchronize access to the ICollection.</summary>
        /// <value>An object that can be used to synchronize access to the ICollection.</value>
        object ICollection.SyncRoot
        {
            get { return (this.list as ICollection).SyncRoot; }
        }

        #endregion

        #region IEntityCollection.Add.

        /// <summary>
        /// Adds an entity to the end of the collection.
        /// </summary>
        /// <param name="entity">The entity to be added to the end of the EntityCollection. 
        /// Nulls are not allowed, otherwise a NoNullAllowedException is generated.
        /// Entity must be an instance of a class compatible with <see cref="IDbTable"/>.</param>
        /// <param name="validateEntityType">Indicates whether the type of entity is validated to
        /// determine if it is an instance of a class compatible with <see cref="IDbTable"/>.
        /// Set to <b>false</b> only when absoulutely sure that the entity is compatibile.
        /// Setting this parameter to <b>false</b> may increase performance.</param>
        void IEntityCollection.Add(IEntity entity, bool validateEntityType)
        {
            // Nothing to check. Cast operator ensures valid entity type and InsertItem method ensures no-null reference.
            Add((TEntity)entity);
        }

        #endregion

        #region Cast operator.

        /// <summary>
        /// Converts the given DataTable to EntityCollection.
        /// </summary>
        /// <param name="data">Contains entity data. Must be compatibile with entities stored in the collection.</param>
        /// <returns>EntityCollection.</returns>
        public static explicit operator EntityCollection<TEntity, TTable>(DataTable data)
        {
            var collection = new EntityCollection<TEntity, TTable>();
            EntityCollectionExtensions.FromDataTable<TEntity>(collection, data);
            return collection;
        }

        #endregion
    }
}
